/*
 * ESArrayWrapper.java - ScriptME
 * 
 * Copyright (c) 2009 Cesar Henriques <cesar at alttab.com.ar>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Based on FESI Project
 * 
 * Contributors:
 * 	Jean-Marc Lugrin - initial API and implementation
 * 	Cesar Henriques <cesar at alttab.com.ar> - J2ME Porting and Extensions
 */
package org.scriptme.ecmascript.data;

import java.util.Enumeration;

import org.scriptme.ecmascript.exceptions.EcmaScriptException;
import org.scriptme.ecmascript.exceptions.ProgrammingError;
import org.scriptme.ecmascript.interpreter.Evaluator;
import org.scriptme.ecmascript.interpreter.ScopeChain;
import org.scriptme.util.Array;

// Class to wrap a Java array as an EcmaScript object
/**
 * The Class ESArrayWrapper.
 */
public class ESArrayWrapper extends ESObject {

	// The java array
	/** The java array. */
	protected Object javaArray;

	/**
	 * Create a new array wrapper.
	 * 
	 * @param javaArray
	 *            the java array to wrap
	 * @param evaluator
	 *            the evaluator
	 */
	public ESArrayWrapper(Object javaArray, Evaluator evaluator) {
		super(null, evaluator);
		this.javaArray = javaArray;
		if (!javaArray.getClass().isArray()) {
			throw new ProgrammingError("Array wrapper used on non array object");
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getPrototype()
	 */
	public ESObject getPrototype() {
		throw new ProgrammingError("Cannot get prototype of Array Wrapper");
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getESClassName()
	 */
	public String getESClassName() {
		return "Java Array";
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getTypeOf()
	 */
	public int getTypeOf() {
		return EStypeObject;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#putProperty(java.lang.String,
	 *      org.scriptme.ecmascript.data.ESValue, int)
	 */
	public void putProperty(String propertyName, ESValue propertyValue, int hash)
			throws EcmaScriptException {
		if (propertyName.equals("length")) {
			int length = (int) (((ESPrimitive) propertyValue).doubleValue());
			if (length < 0) {
				throw new EcmaScriptException("Invalid length value: "
						+ propertyValue);
			}
			throw new EcmaScriptException("length of Java Arrays is immutable");
		} else {
			int index = -1; // indicates not a valid index value
			try {
				index = Integer.parseInt(propertyName); // should be uint
			} catch (NumberFormatException e) {
			}
			if (index < 0) {
				throw new EcmaScriptException(
						"Java Arrays accept only index properties");
			} else {
				putProperty(index, propertyValue);
			}
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#putProperty(int,
	 *      org.scriptme.ecmascript.data.ESValue)
	 */
	public void putProperty(int index, ESValue propertyValue)
			throws EcmaScriptException {

		int l = Array.getLength(javaArray);
		if (index >= l || index < 0) {
			throw new EcmaScriptException("Index " + index
					+ " outside of Java Arrays size of " + l);
		}
		Object obj = propertyValue.toJavaObject();
		try {
			Array.set(javaArray, index, obj);
		} catch (IllegalArgumentException e) {
			String type = "null";
			if (obj != null)
				type = ESLoader.typeName(obj.getClass());
			throw new EcmaScriptException("Cannot store a " + type
					+ " in the java array "
					+ ESLoader.typeName(javaArray.getClass()));
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getPropertyInScope(java.lang.String,
	 *      org.scriptme.ecmascript.interpreter.ScopeChain, int)
	 */
	public ESValue getPropertyInScope(String propertyName,
			ScopeChain previousScope, int hash) throws EcmaScriptException {
		if (propertyName.equals("length")) {
			return new ESNumber((double) Array.getLength(javaArray));
		}
		// Do not examine the integer values...
		if (previousScope == null) {
			throw new EcmaScriptException("global variable '" + propertyName
					+ "' does not have a value");
		} else {
			return previousScope.getValue(propertyName, hash);
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getProperty(java.lang.String,
	 *      int)
	 */
	public ESValue getProperty(String propertyName, int hash)
			throws EcmaScriptException {
		if (propertyName.equals("length")) {
			return new ESNumber(Array.getLength(javaArray));
		} else {
			int index = -1; // indicates not a valid index value
			try {
				index = Integer.parseInt(propertyName); // should be uint
			} catch (NumberFormatException e) {
			}
			if (index < 0) {
				throw new EcmaScriptException(
						"Java Arrays accept only index properties");
			} else {
				return getProperty(index);
			}
		}
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getProperty(int)
	 */
	public ESValue getProperty(int index) throws EcmaScriptException {
		Object theElement = null;
		int l = Array.getLength(javaArray);
		if (index >= l || index < 0) {
			throw new EcmaScriptException("Java Array index " + index
					+ " is out of range " + l);
		}
		theElement = Array.get(javaArray, index);
		return ESLoader.normalizeValue(theElement, evaluator);
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#hasProperty(java.lang.String,
	 *      int)
	 */
	public boolean hasProperty(String propertyName, int hash)
			throws EcmaScriptException {
		if (propertyName.equals("length")) {
			return true;
		} else {
			int index = -1; // indicates not a valid index value
			try {
				index = Integer.parseInt(propertyName); // should be uint
			} catch (NumberFormatException e) {
			}
			if (index < 0) {
				return false;
			} else {
				return (index >= 0) && (index < Array.getLength(javaArray));
			}
		}
	}

	// overrides
	// Skip elements which were never set (are null), as Netscape
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getProperties()
	 */
	public Enumeration getProperties() {
		return new Enumeration() {
			int nextIndex = 0;
			int length = Array.getLength(javaArray);

			public boolean hasMoreElements() {
				while ((nextIndex < length)
						&& (Array.get(javaArray, nextIndex) == null)) {
					nextIndex++;
				}
				return nextIndex < length;
			}

			public Object nextElement() {
				if (hasMoreElements()) {
					return new ESNumber(nextIndex++);
				} else {
					throw new java.util.NoSuchElementException();
				}
			}
		};
	}

	/**
	 * Get all properties (including hidden ones), for the command.
	 * 
	 * @return An enumeration of all properties (visible and hidden).
	 * 
	 * @listall of the interpreter.
	 *          <P>
	 *          An ESArrayWrapper has no prototype, but it has the hidden
	 *          property LENGTH.
	 */
	public Enumeration getAllProperties() {
		return new Enumeration() {
			String[] specialProperties = getSpecialPropertyNames();
			int specialEnumerator = 0;
			Enumeration props = properties.keys(); // all of object properties
			String currentKey = null;
			int currentHash = 0;
			int nextIndex = 0;
			int length = Array.getLength(javaArray);

			public boolean hasMoreElements() {
				// OK if we already checked for a property and one exists
				if (currentKey != null)
					return true;
				// loop on index properties
				if (nextIndex < length) {
					while ((nextIndex < length)
							&& (Array.get(javaArray, nextIndex) == null))
						// ignore null entries
						nextIndex++;
					if (nextIndex < length) {
						currentKey = Integer.toString(nextIndex);
						currentHash = currentKey.hashCode();
						nextIndex++;
						return true;
					}
				}
				// Loop on special properties first
				if (specialEnumerator < specialProperties.length) {
					currentKey = specialProperties[specialEnumerator];
					currentHash = currentKey.hashCode();
					specialEnumerator++;
					return true;
				}
				// loop on standard or prototype properties
				if (props.hasMoreElements()) {
					currentKey = (String) props.nextElement();
					currentHash = currentKey.hashCode();
					return true;
				}
				return false;
			}

			public Object nextElement() {
				if (hasMoreElements()) {
					String key = currentKey;
					currentKey = null;
					return key;
				} else {
					throw new java.util.NoSuchElementException();
				}
			}
		};
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getSpecialPropertyNames()
	 */
	public String[] getSpecialPropertyNames() {
		String[] ns = { "length" };
		return ns;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#isHiddenProperty(java.lang.String,
	 *      int)
	 */
	public boolean isHiddenProperty(String propertyName, int hash) {
		return false;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#putHiddenProperty(java.lang.String,
	 *      org.scriptme.ecmascript.data.ESValue)
	 */
	public void putHiddenProperty(String propertyName, ESValue propertyValue)
			throws EcmaScriptException {
		throw new ProgrammingError("Cannot put hidden property in " + this);
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#deleteProperty(java.lang.String,
	 *      int)
	 */
	public boolean deleteProperty(String propertyName, int hash)
			throws EcmaScriptException {
		return !hasProperty(propertyName, hash); // none can be deleted
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getDefaultValue(int)
	 */
	public ESValue getDefaultValue(int hint) throws EcmaScriptException {
		if (hint == EStypeString) {
			return new ESString(javaArray.toString());
		} else {
			throw new EcmaScriptException("No default value for " + this
					+ " and hint " + hint);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#getDefaultValue()
	 */
	public ESValue getDefaultValue() throws EcmaScriptException {
		return this.getDefaultValue(EStypeString);
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#doubleValue()
	 */
	public double doubleValue() {
		double d = Double.NaN;
		return d;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#booleanValue()
	 */
	public boolean booleanValue() {
		return true;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#toString()
	 */
	public String toString() {
		return (javaArray == null) ? "<?Array Wrapper to null?>"
				: "[object JavaArray]";
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#toJavaObject()
	 */
	public Object toJavaObject() {
		return javaArray;
	}

	// overrides
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.scriptme.ecmascript.data.ESObject#toDetailString()
	 */
	public String toDetailString() {
		return "ES:[" + getESClassName() + ":" + javaArray.toString() + "]";
	}
}
